package com.xcity.robot.net.pomelo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.alibaba.fastjson.JSON;
import com.xcity.robot.net.packet.impl.DefaultBinaryPacket;

import naga.x.App;
import naga.x.net.Packet;
import naga.x.net.impl.pomelo.PomeloException;
import naga.x.net.impl.pomelo.PomeloPacket;
import naga.x.net.impl.pomelo.protocol.Protocol;

public class Message {
	
	public static final int MSG_FLAG_BYTES = 1;
	public static final int MSG_ROUTE_CODE_BYTES = 2;
	public static final int MSG_ID_MAX_BYTES = 5;
	public static final int MSG_ROUTE_LEN_BYTES = 1;
	public static final int MSG_ROUTE_CODE_MAX = 0xffff;

	public static final int MSG_COMPRESS_ROUTE_MASK = 0x1;
	public static final int MSG_COMPRESS_GZIP_MASK = 0x1;
	public static final int MSG_COMPRESS_GZIP_ENCODE_MASK = 1 << 4;
	public static final int MSG_TYPE_MASK = 0x7;

//	public static final int TYPE_REQUEST = 0;
//	public static final int TYPE_NOTIFY = 1;
//	public static final int TYPE_RESPONSE = 2;
//	public static final int TYPE_PUSH = 3;
	
	private static final Logger LOG = LoggerFactory.getLogger(Message.class);
	
	public static enum Type {
		REQUEST,
		NOTIFY,
		RESPONSE,
		PUSH;
		public boolean msgHasId() {
		    return this == REQUEST || this == RESPONSE;
		}
		public boolean msgHasRoute() {
			return this == REQUEST || this == NOTIFY || this == PUSH;
		}
		public static Type codeOf(int code) {
			if (code == 0) return REQUEST;
			if (code == 1) return NOTIFY;
			if (code == 2) return RESPONSE;
			if (code == 3) return PUSH;
			return null;
		}
	}
	
	private int id;
//	private int type;
	private Type type;
	private boolean compressRoute;
	private short routeCode;
	private String route;
	private PomeloPacket packet;
	private boolean compressGzip;
	
	public Message(int id, Type type, boolean compressRoute, short routeCode, PomeloPacket packet, boolean compressGzip) {
		this(id, type, compressRoute, routeCode, App.getApp().getConfig().getDictionary().getRoute(routeCode), packet, compressGzip);
	}
	
	public Message(int id, Type type, boolean compressRoute, String route, PomeloPacket packet, boolean compressGzip) {
		this(id, type, compressRoute, App.getApp().getConfig().getDictionary().getRouteCode(route), route, packet, compressGzip);
	}
	
	public Message(int id, Type type, boolean compressRoute, short routeCode, String route, PomeloPacket packet, boolean compressGzip) {
		this.id = id;
		this.type = type;
		this.compressRoute = compressRoute;
		this.routeCode = routeCode;
		this.route = route;
		this.packet = packet;
		this.compressGzip = compressGzip;
		if (packet != null) {
			packet.setOpCode(routeCode);
			packet.setSerial(id);
		} else {
			LOG.warn("[POMELO]MESSAGE PACKET NULL[" + route + "]");
		}
	}
	
	public int getId() {
		return id;
	}

	public Type getType() {
		return type;
	}

	public boolean isCompressRoute() {
		return compressRoute;
	}

	public short getRouteCode() {
		return routeCode;
	}

	public String getRoute() {
		return route;
	}

	public Packet getPacket() {
		return packet;
	}

	public boolean isCompressGzip() {
		return compressGzip;
	}
	
	/**
	 * Message protocol encode.
	 *
	 * @param  {Number} id            message id
	 * @param  {Number} type          message type
	 * @param  {Number} compressRoute whether compress route
	 * @param  {Number|String} route  route code or route string
	 * @param  {Buffer} msg           message body bytes
	 * @return {Buffer}               encode result
	 * @throws PomeloException 
	 */
	public static byte[] encode(int id, Type type, boolean compressRoute, Object route, Object msg, boolean compressGzip) throws PomeloException {
	    // caculate message max length
	    int idBytes = /*msgHasId(type)*/type.msgHasId() ? caculateMsgIdBytes(id) : 0;
	    int msgLen = MSG_FLAG_BYTES + idBytes;

	    if (/*msgHasRoute(type)*/type.msgHasRoute()) {
	    	if (compressRoute) {
	    		if (!(route instanceof Number)) {
	    			route = App.getApp().getConfig().getDictionary().getRouteCode(route.toString());
//	    			if (route == null) {
//	    				throw new PomeloException("error flag for number route: " + route);
//	    			}
	    		} else {
	    			route = ((Number) route).shortValue();
	    		}
	    		msgLen += MSG_ROUTE_CODE_BYTES;
	    	} else {
	    		msgLen += MSG_ROUTE_LEN_BYTES;
	    		if (route != null) {
	    			byte[] _route = Protocol.strencode(route.toString());
	    			int len = _route.length;
	    			if (len > 255) {
	    				throw new PomeloException("route maxlength is overflow: " + len);
	    			}
	    			route = _route;
	    			msgLen += len;
	    		}
	    	}
	    } else {
	    	compressRoute = false;
	    }

	    byte[] msgBytes = null;
	    if (msg != null) {
	    	msgBytes = JSON.toJSONBytes(msg);
	    	msgLen += msgBytes.length;
	    }

	    byte[] buffer = new byte[msgLen];
	    int offset = 0;

	    // add flag
	    offset = encodeMsgFlag(type, compressRoute, buffer, offset, compressGzip);

	    // add message id
	    if (/*msgHasId(type)*/type.msgHasId()) {
	    	offset = encodeMsgId(id, buffer, offset);
	    }

	    // add route
	    if (/*msgHasRoute(type)*/type.msgHasRoute()) {
	    	offset = encodeMsgRoute(compressRoute, route, buffer, offset);
	    }

	    // add body
	    if (msgBytes != null) {
	    	offset = encodeMsgBody(msgBytes, buffer, offset);
	    }
	    return buffer;
	}
	
	public static byte[] encode(Message message) throws PomeloException {
		return encode(message.id, message.type, message.compressRoute, message.route, message.packet, message.compressGzip);
	}
	
	/**
	 * Message protocol decode.
	 *
	 * @param  {Buffer|Uint8Array} buffer message bytes
	 * @return {Object}            message object
	*/
	public static Message decode(byte[] bytes, Class<?> bodyType) {
	    int bytesLen = bytes.length;
	    int offset = 0;
	    int id = 0;
	    short routeCode = -1;
	    String route = null;

	    // parse flag
	    int flag = bytes[offset++];
	    boolean compressRoute = (flag & MSG_COMPRESS_ROUTE_MASK) != 0;
	    int _type = (flag >> 1) & MSG_TYPE_MASK;
	    Type type = Type.codeOf(_type);
	    if (type == null) {
	    	throw new NullPointerException("Pomelo message decode error: type is not valid " + _type);
	    }
	    boolean compressGzip = ((flag >> 4) & MSG_COMPRESS_GZIP_MASK) != 0;

	    // parse id
	    if (/*msgHasId(type)*/type.msgHasId()) {
	    	int m = 0;
	    	int i = 0;
	    	do {
	    		m = bytes[offset];
	    		id += (m & 0x7f) << (7 * i);
	    		offset++;
	    		i++;
	    	} while (m >= 128);
	    }

	    // parse route
	    if (/*msgHasRoute(type)*/type.msgHasRoute()) {
	    	if (compressRoute) {
	    		routeCode = (short) (bytes[offset++] << 8 | bytes[offset++]);
	    		if (bodyType == null) {
	    			bodyType = App.getApp().getConfig().getDictionary().getRequestClass((Short) routeCode);
	    		}
	    	} else {
	    		int routeLen = bytes[offset++];
	    		if (routeLen > 0) {
	    			byte[] _route = new byte[routeLen];
	    			System.arraycopy(bytes, offset, _route, 0, routeLen);
	    			route = Protocol.strdecode(_route);
	    		} else {
	    			route = "";
	    		}
	    		offset += routeLen;
	    		if (bodyType == null) {
	    			bodyType = App.getApp().getConfig().getDictionary().getRequestClass((String) route);
	    		}
	    	}
	    }

	    // parse body
	    int bodyLen = bytesLen - offset;
	    byte[] body = new byte[bodyLen];
	    
	    System.arraycopy(bytes, offset, body, 0, bodyLen);

	    if (bodyType == null) {
	    	return new Message(id, type, compressRoute, routeCode, route, new DefaultBinaryPacket(id, body), compressGzip);
	    }
	    return deserialize(id, type, compressRoute, routeCode, route, body, compressGzip, bodyType);
	}
	
	public static Message decode(byte[] bytes) {
		return decode(bytes, null);
	}
	
//	public static Message deserialize(Message src, Class<?> bodyType) {
//		if (src.body instanceof byte[]) {
//			return deserialize(src.id, src.type, src.compressRoute, src.routeCode, src.route, (byte[]) src.body, src.compressGzip, bodyType);
//		}
//		return src;
//	}
	
	private static Message deserialize(int id, Type type, boolean compressRoute, short routeCode, String route, byte[] body, boolean compressGzip, Class<?> bodyType) {
		if (body == null) {
			return new Message(id, type, compressRoute, route, null, compressGzip);
		}
//		if (compressRoute) {
//			if (App.getApp().getConfig().getBooleanValue(Config.POMELO_IS_USE_DICT)) {
//			      Map<String, String> abbrs = App.getApp().getConfig().getDictionary().getAbbrs();
//			      String _route = abbrs.get(String.valueOf(route));
//			      if (_route == null) {
//			    	  LOG.error("dictionary error! no abbrs for route : " + route);
////			    	  return new Message<T>(id, type, compressRoute, route, null, compressGzip);
//			      } else {
//			    	  route = _route;
//			      }
//			}/* else {
//				LOG.error("fail to uncompress route code for msg: " + id + ", server not enable dictionary.");
//				return new Message<T>(id, type, compressRoute, route, null, compressGzip);
//			}*/
//		}
		
//		// decode use protobuf
//		if (!!this.protobuf && !!this.protobuf.getProtos().client[route]) {
//			msg.body = this.protobuf.decode(route, msg.body);
//		} else if (!!this.decodeIO_protobuf && !!this.decodeIO_protobuf.check(Constants.RESERVED.CLIENT, route)) {
//			msg.body = this.decodeIO_protobuf.decode(route, msg.body);
//		} else {
//			try {
//				msg.body = JSON.parse(msg.body.toString('utf8'));
//			} catch (ex) {
//				msg.body = {};
//			}
//		}
		
//		try {
//			bodyJson = JSON.parseObject(new String(body, IOUtils.UTF8));
//			body = null; // free
//		} catch (UnsupportedEncodingException e) {
//		}
		PomeloPacket packet = null;
		try {
			packet = JSON.parseObject(body, bodyType);
		} catch (Exception e) {
			LOG.error(e.getMessage(), e);
		}
		return new Message(id, type, compressRoute, routeCode, route, packet, compressGzip);
	}
	
//	private static boolean msgHasId(int type) {
//	    return type == TYPE_REQUEST || type == TYPE_RESPONSE;
//	}
//	
//	private static boolean msgHasRoute(int type) {
//		return type == TYPE_REQUEST || type == TYPE_NOTIFY || type == TYPE_PUSH;
//	}
	
	private static int caculateMsgIdBytes(int id) {
		int len = 0;
		do {
			len += 1;
			id >>= 7;
		} while (id > 0);
		return len;
	}
	
	private static int encodeMsgFlag(Type type, boolean compressRoute, byte[] buffer, int offset, boolean compressGzip) throws PomeloException {
//		if (type != TYPE_REQUEST && type != TYPE_NOTIFY
//				&& type != TYPE_RESPONSE && type != TYPE_PUSH) {
//			throw new PomeloException("unkonw message type: " + type);
//	    }

	    buffer[offset] = (byte) ((type.ordinal() << 1) | (compressRoute ? 1 : 0));

	    if (compressGzip) {
	    	buffer[offset] = (byte) (buffer[offset] | MSG_COMPRESS_GZIP_ENCODE_MASK);
	    }

	    return offset + MSG_FLAG_BYTES;
	}

	private static int encodeMsgId(int id, byte[] buffer, int offset) {
		do {
			int tmp = id % 128;
			int next = (int) Math.floor(id / 128);

			if (next != 0) {
				tmp = tmp + 128;
			}
			buffer[offset++] = (byte) tmp;

			id = next;
	    } while (id != 0);

	    return offset;
	}

	private static int encodeMsgRoute(boolean compressRoute, Object route, byte[] buffer, int offset) throws PomeloException {
		if (compressRoute) {
			int _route;
			if (!(route instanceof Number) || (_route = ((Number) route).intValue()) > MSG_ROUTE_CODE_MAX) {
				throw new PomeloException("route number is overflow: " + route);
			}
			buffer[offset++] = (byte) ((_route >> 8) & 0xff);
			buffer[offset++] = (byte) (_route & 0xff);
		} else {
			if (route != null) {
				if (!(route instanceof byte[])) {
					throw new PomeloException("route is not byte[]: " + route);
				}
				byte[] _route = (byte[]) route;
				int len = _route.length;
				buffer[offset++] = (byte) (len & 0xff);
				System.arraycopy(_route, 0, buffer, offset, len);
				offset += len;
			} else {
				buffer[offset++] = 0;
			}
		}

		return offset;
	}

	private static int encodeMsgBody(byte[] msg, byte[] buffer, int offset) {
		int len = msg.length;
		System.arraycopy(msg, 0, buffer, offset, len);
		return offset + len;
	}
	
}
